﻿using System;
using System.ComponentModel;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Altairis.Web.UI.WebControls {

    [ValidationProperty("IsVerified"), Themeable(true)]
    public class ReCaptchaImage : Control {
        private const string DEFAULT_LANGUAGE = "en";
        private const string DEFAULT_THEME = "red";
        private const string CHALLENGE_FIELD_NAME = "recaptcha_challenge_field";
        private const string CHALLENGE_URL = "http://www.google.com/recaptcha/api/challenge?k={0}&error={1}";
        private const string CHALLENGE_URL_SECURE = "https://www.google.com/recaptcha/api/challenge?k={0}&error={1}";
        private const string INIT_SCRIPT = "var RecaptchaOptions = {{theme: '{0}', lang: '{1}'}};";
        private const string RESPONSE_FIELD_NAME = "recaptcha_response_field";
        private const string RESPONSE_INVALID_ERR = "incorrect-captcha-sol";
        private const string RESPONSE_URL = "http://www.google.com/recaptcha/api/verify";
        private string verificationErrorMessage = string.Empty;
        private bool verificationState = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="ReCaptchaImage"/> class.
        /// </summary>
        public ReCaptchaImage() {
            this.Theme = DEFAULT_THEME;
        }

        /// <summary>
        /// Gets the challenge text returned via form data.
        /// </summary>
        /// <value>The challenge text.</value>
        [Browsable(false)]
        public string Challenge {
            get {
                if (!this.Page.IsPostBack) throw new InvalidOperationException("This property is available only during postback.");
                return this.Page.Request.Form[CHALLENGE_FIELD_NAME];
            }
        }

        /// <summary>
        /// Gets a value indicating whether this CAPTCHA is verified.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this CAPTCHA is verified; otherwise, <c>false</c>.
        /// </value>
        [Browsable(false)]
        public bool IsVerified {
            get {
                if (!this.Page.IsPostBack) throw new InvalidOperationException("This property is available only during postback.");
                return verificationState;
            }
        }

        /// <summary>
        /// Gets or sets the language.
        /// </summary>
        /// <value>The two-letter ISO language code.</value>
        [Category("Appearance"), Themeable(true), DefaultValue(DEFAULT_LANGUAGE)]
        [Description("Two-letter ISO language code. If unspecified, UiCulture is used. Supported values are \"en\", \"nl\", \"fr\", \"de\", \"pt\", \"ru\", \"es\" and \"tr\".")]
        public string Language {
            get { return (string)this.ViewState["Language"]; }
            set { this.ViewState["Language"] = value; }
        }

        /// <summary>
        /// Gets or sets the private key.
        /// </summary>
        /// <value>The private key.</value>
        [Category("Behavior")]
        [Description("Private key registered on ReCaptcha web site. You can get one at https://www.google.com/recaptcha/admin/create?app=Altairis.Web.UI.")]
        public string PrivateKey {
            get { return (string)this.ViewState["PrivateKey"]; }
            set { this.ViewState["PrivateKey"] = value; }
        }

        /// <summary>
        /// Gets or sets the public key.
        /// </summary>
        /// <value>The public key.</value>
        [Category("Behavior")]
        [Description("Public key registered on ReCaptcha web site. You can get one at https://www.google.com/recaptcha/admin/create?app=Altairis.Web.UI.")]
        public string PublicKey {
            get { return (string)this.ViewState["PublicKey"]; }
            set { this.ViewState["PublicKey"] = value; }
        }

        /// <summary>
        /// Gets the response text returned via form data.
        /// </summary>
        /// <value>The response text.</value>
        [Browsable(false)]
        public string Response {
            get {
                if (!this.Page.IsPostBack) throw new InvalidOperationException("This property is available only during postback.");
                return this.Page.Request.Form[RESPONSE_FIELD_NAME];
            }
        }

        /// <summary>
        /// Gets or sets the client API theme.
        /// </summary>
        /// <value>The client API theme.</value>
        [Category("Appearance"), Themeable(true), DefaultValue(DEFAULT_THEME)]
        [Description("The client API theme of ReCaptcha control. Supported values are \"red\", \"white\", \"blackglass\" and \"clean\".")]
        public string Theme {
            get { return (string)this.ViewState["Theme"]; }
            set { this.ViewState["Theme"] = value; }
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Init"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            this.Language = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
        }

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Load"/> event.
        /// </summary>
        /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param>
        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);

            // Skip all processing in design mode
            if (this.DesignMode) return;

            // Validate keys
            if (string.IsNullOrEmpty(this.PublicKey)) throw new ArgumentException("PublicKey property cannot be null or empty.");
            if (string.IsNullOrEmpty(this.PrivateKey)) throw new ArgumentException("PrivateKey property cannot be null or empty.");

            // Verify captcha solution on postback
            if (this.Page.IsPostBack) this.VerifyCaptchaSolution();
        }

        /// <summary>
        /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object, which writes the content to be rendered on the client.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param>
        protected override void Render(HtmlTextWriter writer) {
            // Render empty DIV in design time
            if (this.DesignMode) {
                this.DesignModeRender(writer);
                return;
            }

            // If theme/language difers from default, render init script
            if (!DEFAULT_THEME.Equals(this.Theme) || !DEFAULT_LANGUAGE.Equals(this.Language)) {
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
                writer.RenderBeginTag(HtmlTextWriterTag.Script);
                writer.Write(INIT_SCRIPT, this.Theme, this.Language);
                writer.RenderEndTag();
            }

            // Call control creation script
            writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/javascript");
            writer.AddAttribute(HtmlTextWriterAttribute.Src, this.GetChallengeUrl());
            writer.RenderBeginTag(HtmlTextWriterTag.Script);
            writer.RenderEndTag();
        }

        /// <summary>
        /// Renders the control in design mode.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param>
        private void DesignModeRender(HtmlTextWriter writer) {
            writer.AddStyleAttribute(HtmlTextWriterStyle.Width, "304px");
            writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "115px");
            writer.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "2px");
            writer.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "dashed");
            writer.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "black");
            writer.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "white");
            writer.AddStyleAttribute(HtmlTextWriterStyle.Color, "black");
            writer.AddStyleAttribute(HtmlTextWriterStyle.Padding, "5px");
            writer.RenderBeginTag(HtmlTextWriterTag.Div);
            writer.Write("ReCaptchaImage");
            writer.RenderEndTag();
        }

        /// <summary>
        /// Gets the challenge URL.
        /// </summary>
        /// <returns>URL of Challenge API.</returns>
        private string GetChallengeUrl() {
            if (this.Page.Request.IsSecureConnection) return string.Format(CHALLENGE_URL_SECURE, this.PublicKey, this.verificationErrorMessage);
            else return string.Format(CHALLENGE_URL, this.PublicKey, this.verificationErrorMessage);
        }

        /// <summary>
        /// Verifies the captcha solution.
        /// </summary>
        private void VerifyCaptchaSolution() {
            // Check if we have required form fields
            if (string.IsNullOrEmpty(this.Challenge) || string.IsNullOrEmpty(this.Response)) {
                this.verificationState = false;
                this.verificationErrorMessage = RESPONSE_INVALID_ERR;
            }

            // Build POST data
            var formData = new System.Collections.Specialized.NameValueCollection();
            formData.Add("privatekey", this.PrivateKey);
            formData.Add("remoteip", this.Page.Request.UserHostAddress);
            formData.Add("challenge", this.Challenge);
            formData.Add("response", this.Response);

            // Get response
            string responseString;
            using (var wc = new System.Net.WebClient()) {
                var responseData = wc.UploadValues(RESPONSE_URL, "POST", formData);
                responseString = System.Text.Encoding.UTF8.GetString(responseData, 0, responseData.Length);
            }

            // Parse response
            var responseLines = responseString.Split('\n');
            if (responseLines[0].Equals("true")) {
                this.verificationState = true;
                this.verificationErrorMessage = string.Empty;
            }
            else {
                this.verificationState = false;
                this.verificationErrorMessage = responseLines[1];
            }
        }
    }

    public class ReCaptchaValidator : BaseValidator {

        /// <summary>
        /// Determines whether the value in the input control is valid.
        /// </summary>
        /// <returns>
        /// true if the value in the input control is valid; otherwise, false.
        /// </returns>
        protected override bool EvaluateIsValid() {
            bool isValid;
            bool isBool = bool.TryParse(this.GetControlValidationValue(this.ControlToValidate), out isValid);
            return isBool && isValid;
        }
    }
}